<?php
/**
 * Spiffy Library
 *
 * LICENSE
 * 
 * This source file is subject to the new BSD license that is
 * available through the world-wide-web at this URL:
 * http://www.spiffyjr.me/license
 * 
 * @category   	Spiffy
 * @package    	Spiffy_Db
 * @copyright  	Copyright (c) 2009 Kyle Spraggs. http://www.spiffyjr.me
 * @license 	http://www.spiffyjr.me/license	New BSD License
 * @author 		Kyle Spraggs <theman@spiffyjr.me>
 */

/**
 * Class to provide a domain object for a table data gateway.
 * 
 * @category	Spiffy
 * @package		Spiffy_Db
 * @copyright  	Copyright (c) 2009 Kyle Spraggs. http://www.spiffyjr.me
 * @license 	http://www.spiffyjr.me/license	New BSD License 
 */
class Spiffy_Db_Model
{
	// Data save() options
	const IGNORE = 'onSaveIgnore';
	const PRIMARY = 'onSavePrimary';
	
	// toArray() options
	const ARRAY_USE_NAME = 'arrayUseName';
	const ARRAY_USE_COLUMN = 'arrayUseColumn';
	
	/**
	 * Array of data to define how model data is mapped 
	 * to the table data gateway.
	 * @var array
	 */
	protected $_data = array();
	
	/**
	 * Keeps track of the Zend_Filter_Input for the model and is only
	 * populated once unless explicitly requested.
	 * @var Zend_Filter_Input
	 */
	protected $_filterInput = null;
	
	/**
	 * Keeps the instances of each filter so we don't need to
	 * create new instances when unncessary.
	 */
	protected $_filters = array();
	
	/**
	 * Instance of the data mapper which maps this domain 
	 * object to the database.
	 * @var Spiffy_Db_Model_Mapper
	 */
	protected $_mapper = null;
	
	/**
	 * Class name of the mapper. Initialized on construction if
	 * not set explicitly.
	 * @var string
	 */
	protected $_mapperClass = null;
	
	/**
	 * Reverse lookup cache for column name to data name.
	 */
	protected $_reverseCache = array();
	
	/**
	 * Keeps the instances of each validator so we don't need to
	 * create new instances when unncessary.
	 */
	protected $_validators = array();
	
	/**
	 * Class constructor.
	 */
	public function __construct(array $data = array())
	{
		// Initialize the mapper name
		if (null === $this->_mapperClass) {
			$this->_mapperClass = get_class($this) . 'Mapper';
		}
		
		// Ensure a column is set for each data key (if not, try automatic)
		foreach ($this->_data as $key => $value) {
			if (!is_array($value)) {
				unset($this->_data[$key]);
				$key = $value;
				$value = array();
			}
			
			if (!array_key_exists('column', $value)) {
				$value['column'] = $this->_convertCamelCase($key);
				$this->_data[$key] = $value;
			}
			
			if (!array_key_exists('validators', $value)) {
				$value['validators'] = array();
			} else {
				if (is_string($value['validators'])) {
					$value['validators'] = array($value['validators']);
				}
			}
			
			if (!array_key_exists('filters', $value)) {
				$value['filters'] = array();
			} else {
				if (is_string($value['filters'])) {
					$value['filters'] = array($value['filters']);
				}
			}
			
			$this->_data[$key] = $value;
		}
		
		// Set default data
		if (is_array($data)) {
			$this->setValues($data);
		}
		
		$this->init();
	}
	
	/**
	 * Provides magic method functionality.
	 * @throws Spiffy_Db_Model_Exception if no method exists.
	 */
	public function __call($method, array $args)
	{
		$matches = array();
		
		/**
		 * Recognize methods for case:
		 * set<Data>()
		 * Use the greedy pattern repeat modifier e.g. \w+
		 */
		if (preg_match('/^set(\w+)$/', $method, $matches)) {
			$name = str_replace('set', '', $method);
			$this->setValue($name, $args[0]);
			return $this;
		}
		
		/**
		 * Recognize methods for case:
		 * get<Data>()
		 * Use the greedy pattern repeat modifier e.g. \w+
		 */
		if (preg_match('/^get(\w+)$/', $method, $matches)) {
			$name = str_replace('get', '', $method);
			return $this->getValue($name);
		}
		
		throw new Spiffy_Db_Model_Exception('Unknown method ' . $method . ' for class ' . get_class($this));
	}
	
	/**
	 * Accessor.
	 * @param string $name
	 * @return variable
	 */
	public function __get($name)
	{
		return $this->getValue($name);
	}
	
	/**
	 * Mutator.
	 * @param string $name
	 * @param string $value
	 */
	public function __set($name, $value)
	{
		return $this->setValue($name, $value);
	}
	
	/**
	 * Deletes a row using the table data gateway.
	 * 
	 * @param array|string $where Where clause for deleting.
	 */
	public function delete($where)
	{
		$this->mapper()->delete($where);
		return $this;
	}
	
	/**
	 * Fetches all data from the table data gateway using
	 * the domain object mapper and returns an array of domain objects
	 * (one per row).
	 * @param string|array|Zend_Db_Table_Select $where  OPTIONAL An SQL WHERE clause or Zend_Db_Table_Select object.
	 * @param string|array                      $order  OPTIONAL An SQL ORDER clause.
	 * @param int                               $count  OPTIONAL An SQL LIMIT count.
	 * @param int                               $offset OPTIONAL An SQL LIMIT offset.
	 * @return Zend_Db_Table_Rowset_Abstract The row results per the Zend_Db_Adapter fetch mode.
	 * @return array
	 */
	public function fetchAll($where = null, $order = null, $count = null, $offset = null)
	{
		return $this->mapper()->fetchAll(get_class($this), $where, $order, $count, $offset);
	}
	
	/**
	 * Filters a value based on a data filter.
	 * @param string $field Name of the data item to grab filters from.
	 * @param variable $value Value to filter.
	 * @return null|$value Null if $field does not exist otherwise the filtered value
	 */
	public function filter($field, $value)
	{
		if (!array_key_exists($field, $this->_data)) {
			return false;
		}
		
		$filters = array();
		foreach ($this->getFilters($field) as $filter) {
			$filters[] = $this->_loadFilter($filter);
		}
		
		$filterChain = new Zend_Filter();
		foreach ($filters as $filter) {
			$filterChain->addFilter($filter);
		}
		
		return $filterChain->filter($value);
	}
	
	/**
	 * Finds data based on primary key from the table data gateway
	 * using the domain object mapper.
	 * @return $this
	 */
	public function find($id)
	{
		$this->mapper()->find($id, $this);
		return $this;
	}
	
	/**
	 * Gets a joined select from the table data gateway using the
	 * domain object mapper.
	 */
	public function get($cols = array('*'))
	{
		return $this->mapper()->get($cols);
	}
	
	/**
	 * Initialization intended to be used by child classes.
	 */
	public function init()
	{}
	
	/**
	 * Checks if all set model data is valid based on validators/filters
	 * set in the _data map.
	 * @param string $field Data key to check.
	 * @param bool $force If true will force a populate on the valid cache.
	 */
	public function isValid($field = null, $value = null, $force = false)
	{
		if (!array_key_exists($field, $this->_data)) {
			return false;
		}
		
		// Populate the validators
		$this->_populateValid($force);
		
		if (null === $value) {
			return $this->_filterInput->isValid($field);
		}
		
		$inputFilter = $this->_filterInput;
		$inputFilter->setData(array($field => $value));
		
		return $inputFilter->isValid();
	}
	
	/**
	 * Table data gateway mapper.
	 * @return Spiffy_Db_Model_Mapper
	 */
	public function mapper()
	{
		if (null === $this->_mapper) {
			$this->setMapper($this->_mapperClass);
		}
		
		return $this->_mapper;
	}
	
	/**
	 * Query for a resultSet of data using a predefined
	 * statement from get()
	 */
	public function query($reset = true)
	{
		return $this->mapper()->query(get_class($this), $reset);
	}
	
	/**
	 * Query for a single row of data using a predefined
	 * statement from get()
	 * @return $this
	 */
	public function queryOne($reset = true)
	{
		$this->mapper()->queryOne($this, $reset);
		return $this;
	}
	
	/**
	 * Resets the model.
	 * @return $this
	 */
	public function reset()
	{
		foreach (array_keys($this->_data) as $key) {
			if (isset($this->_data[$key]['value'])) {
				$this->_data[$key]['value'] = null;
			}
		}
		return $this;
	}
	
	/**
	 * Saves data to the table data gateway using the domain object mapper.
	 */
	public function save()
	{
		$this->mapper()->save($this);
		return $this;
	}
	
	/**
	 * Returns all data as a $key => $value paired array.
	 * @param string $key Specify which value to use as the key. 
	 * 		Should be one of:
	 * 			-	self::ARRAY_USE_NAME
	 * 			-	self::ARRAY_USE_COLUMN (default) 
	 * @param bool $showNull If true will keep keys for null values (default false).
	 * @return array
	 */
	public function toArray($name = self::ARRAY_USE_COLUMN, $showNull = false)
	{
		$data = array();
		foreach ($this->_data as $key => $value) {
			if ((isset($value['value']) && null !== $value['value']) || $showNull) {
				if ($name == self::ARRAY_USE_COLUMN) {
					$key = $value['column'];
				}
				
				$data[$key] = isset($value['value']) ? $value['value'] : null;
			}
		}
		
		return $data;
	}
	
	/**
	 * Filters the get() query based on the parameters from
	 * GET/POST.
	 * @param array $params Params to include, all by default.
	 */
	public function byParams(array $params = array())
	{
		return $this->mapper()->byParams($this, $params);
	}
	
	/**
	 * Gets a column name based on a data name.
	 * @param string $name Keyed name of the data item from which to get the column name.
	 * @return string
	 */
	public function getColumn($name)
	{
		return $this->_data[$name]['column'];
	}
	
	/**
	 * Gets named data filters.
	 * @param string $name Keyed name to retrieve filters from.
	 * @return string|array
	 */
	public function getFilters($name)
	{
		if (!array_key_exists($name, $this->_data)) {
			throw new Spiffy_Db_Model_Exception(
				'Key ' . $name . ' does not exist in dataset while attempting to retrieve filters.');
		}
		return $this->_data[$name]['filters'];
	}
	
	/**
	 * Data save getter
	 * @param string $name Keyed name to retrieve save information from.
	 * @return string
	 */
	public function getSave($name)
	{
		return array_key_exists('onSave', $this->_data[$name]) ? $this->_data[$name]['onSave'] : false;
	}
	
	/**
	 * Gets named data validators.
	 * @param string $name Keyed name to retrieve validators from.
	 * @return string|array
	 */
	public function getValidators($name)
	{
		return $this->_data[$name]['validators'];
	}
	
	/**
	 * Data value getter.
	 * @param string $name Name of data key to retrieve
	 * @throws Spiffy_Db_Model_Exception if no data is found for the given $name key
	 * @return $this
	 */
	public function getValue($name)
	{
		if (array_key_exists($name, $this->_data)) {
			return isset($this->_data[$name]['value']) ? $this->_data[$name]['value'] : null;
		} else {
			throw new Spiffy_Db_Model_Exception(
				'Key ' . $name . ' does not exist in ' . get_class($this) . ' data for getValue()');
		}
	}
	
	/**
	 * Returns the Zend_Filter_Input object.
	 * @return Zend_Filter_Input
	 */
	public function getFilterInput()
	{
		return $this->_filterInput;
	}
	
	/**
	 * Gets a data name based on a column name.
	 * @param string $column Column name to find
	 */
	public function getNameFromColumn($column)
	{
		if (!isset($this->_reverseCache[$column])) {
			$this->_reverseCache[$column] = null;
			
			foreach ($this->_data as $key => $value) {
				if ($value['column'] == $column) {
					$this->_reverseCache[$column] = $key;
					break;
				}
			}
		}
		
		return $this->_reverseCache[$column];
	}
	
	/**
	 * Sets the mapper.
	 * @param variable $mapper Mapper to set.
	 * @throws Spiffy_Db_Model_Exception if mapper class does not exist or is 
	 * 		   not an instance of Spiffy_Db_Model_Mapper.
	 * @return $this
	 */
	public function setMapper($mapper)
	{
		if (is_string($mapper)) {
			if (!class_exists($mapper)) {
				throw new Spiffy_Db_Model_Exception(
					'Mapper class ' . $mapper . ' could not be initialized. Does it exist?');
			}
			$mapper = new $mapper();
		}
		
		if (!$mapper instanceof Spiffy_Db_Model_Mapper) {
			throw new Spiffy_Db_Model_Exception(
				'Class ' . $mapper . ' exists but is not an instance of Spiffy_Db_Model_Mapper');
		}
		
		$this->_mapper = $mapper;
		return $this;
	}
	
	/**
	 * Data value setter.
	 * @param string $name Name of data key to set
	 * @param string $value Value to set
	 * @throws Spiffy_Db_Model_Exception if no data is found for the given $name key
	 * @return $this
	 */
	public function setValue($name, $value, $filter = true)
	{
		if (array_key_exists($name, $this->_data)) {
			if ($filter) {
				foreach ($this->getFilters($name) as $filter) {
					try {
						$value = Zend_Filter::filterStatic($value, $filter);
					} catch (Zend_Exception $e) {
						throw new Spiffy_Db_Model_Exception(
							'You specified an invalid filter "' . $filter . '" for field "' . $name . '" when 
							attempting to filter value "' . $value . '"');
					}
				}
			}
			$this->_data[$name]['value'] = $value;
		} else {
			throw new Spiffy_Db_Model_Exception(
				'Key ' . $name . ' does not exist in ' . get_class($this) . ' data for setValue()');
		}
		return $this;
	}
	
	/**
	 * Sets all data values
	 * @param array $data Array of key => value pairs to set data with
	 * @param bool $reset If true will reset the values prior to setting them. (default false)
	 * @return $this
	 */
	public function setValues($data, $reset = false)
	{
		if ($reset) {
			$this->reset();
		}
		
		foreach ($data as $name => $value) {
			if (array_key_exists($name, $this->_data)) {
				$this->setValue($name, $value);
			}
		}
		
		return $this;
	}
	
	/**
	 * Converts CamelCase to lowercase with Cameled words being
	 * split using an underscore (_). This function is used
	 * on class construction to attempt to auto set columns
	 * if one isn't provided for the field name.
	 * 
	 * @param string $string String to convert
	 * @return string
	 */
	protected function _convertCamelCase($string)
	{
		$inflector = new Zend_Filter_Inflector(':word');
		$inflector->setRules(array(':word' => array('Word_CamelCaseToUnderscore', 'StringToLower')));
		
		return $inflector->filter(array('word' => $string));
	}
	
	/**
	 * Gets an instance of a filter given by $name
	 * or creates one if it doesn't exist.
	 * @param $name Filter name
	 * @return Zend_Filter|false false if doesn't exist, else validator
	 */
	protected function _loadFilter($name)
	{
		if (!isset($this->_filters[$name])) {
			$name = 'Zend_Filter_' . $name;
			if (!class_exists($name)) {
				return false;
			}
			
			$this->_filters[$name] = new $name();
		}
		
		return $this->_filters[$name];
	}
	
	/**
	 * Checks if model is valid based on filters/validators presented
	 * in the _data table.
	 */
	protected function _populateValid($force = false)
	{
		if (null === $this->_filterInput || $force) {
			$data = $this->toArray(self::ARRAY_USE_NAME);
			$filters = array();
			$validators = array();
			
			foreach ($this->_data as $key => $value) {
				if (isset($value['filters'])) {
					$filters[$key] = $value['filters'];
				}
				
				if (isset($value['validators'])) {
					$validators[$key] = $value['validators'];
				}
			}
			
			$input = new Zend_Filter_Input($filters, $validators, $data);
			$this->_filterInput = $input;
		}
	}
}